using System;
using System.Collections;
using System.Threading;

namespace JK.EventRouter.Threading
{
    /// <summary>
    /// A simple FIFO that blocks for a specified timeout, optionally infinite (the default).
    /// </summary>
    public class BlockingQueue<T> : IDisposable where T : class
    {
        private readonly Queue _queue;
        private bool exit = false;

        #region Constructors

        /// <summary>
        /// Create instance of Queue with the default number of elements. If the queue
        /// is empty, a call to Dequeue will block until another thread calls
        /// Enqueue.
        /// </summary>
        public BlockingQueue()
            : this(10)
        { }
        
        /// <summary>
        /// Create instance of Queue with the initial number of elements. If the queue
        /// is empty, a call to Dequeue will block until another thread calls
        /// Enqueue.
        /// </summary>
        /// <param name="initialSize"></param>
        public BlockingQueue(int initialSize)
        {
            _queue = new Queue(initialSize);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the object values currently in the queue.  If queue is empty, this
        /// will return a zero length array.  This method does not modify the queue, but returns a shallow copy
        /// of the queue buffer containing the objects contained in the queue.
        /// </summary>
        public object[] Values
        {
            get
            {
                lock (_queue.SyncRoot)
                {
                    return _queue.ToArray();
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds an object to the end of the queue. This method will not block.
        /// </summary>
        /// <param name="value"></param>
        public void Enqueue(T value)
        {
            if (value != null)
            {
                lock (_queue.SyncRoot)
                {
                    _queue.Enqueue(value);

                    if (_queue.Count == 1) // Could have blocking Dequeue thread(s).
                    {
                        Monitor.PulseAll(_queue.SyncRoot);
                    }
                }
            }
        }

        /// <summary>
        /// Gracefully shutdown the queue if in an infinite Dequeue
        /// </summary>
        public void Close()
        {
            if (!Monitor.TryEnter(_queue.SyncRoot, 50))
            {
                //TODO : Need logging
                //"unable to aquire lock."
            }
            else
            {
                exit = true;
                Monitor.PulseAll(_queue.SyncRoot);
                Monitor.Exit(_queue.SyncRoot);
            }
        }
        /// <summary>
        /// Removes and returns the object at the beginning of the Queue.
        /// If queue is empty, method will block until another thread calls one of
        /// the Enqueue methods.   This method will wait "Timeout.Infinite" until another
        /// thread Enqueues and object.
        /// </summary>
        /// <returns></returns>
        public T Dequeue()
        {
            return Dequeue(Timeout.Infinite);
        }

        /// <summary>
        /// Removes and returns the object at the beginning of the Queue.
        /// If queue is empty, method will block until another thread calls one of
        /// the Enqueue methods or millisecondsTimeout expires.
        /// If timeout, method will throw QueueTimeoutException.
        /// </summary>
        /// <returns>The object that is removed from the beginning of the Queue.</returns>
        public T Dequeue(int millisecondsTimeout)
        {
            T value = default(T);
            lock (_queue.SyncRoot)
            {
                while (_queue.Count == 0 && exit == false)
                {
                    try
                    {
                        if (!Monitor.Wait(_queue.SyncRoot, millisecondsTimeout, true))
                        {
                            throw new QueueTimeoutException("Dequeue");
                        }
                        // if Pulsed by Close method there is nothing in the queue, so shut down.
                        if (Count == 0) break;
                    }
                        // Allow the caller to handle the exception. A common exception here is
                        // ThreadInterruptedException.
                    catch
                    {
                        Monitor.PulseAll(_queue.SyncRoot);
                        throw;

                    }
                }

                if (_queue.Count > 0)
                {
                    value = dequeue();
                }
            }
            return value;
        }

        /// <summary>
        /// Non-blocking version of Dequeue.  Will return false if queue is empty and set
        /// value to null, otherwise will return true and set value to the dequeued object.
        /// </summary>
        /// <param name="value">The object that is removed from the beginning of the Queue or null if empty.</param>
        /// <returns>true if successfull, otherwise false.</returns>
        public bool TryDequeue(out T value)
        {
            lock (_queue.SyncRoot)
            {
                if (_queue.Count == 0)
                {
                    value = default(T);
                    return false;
                }
                value = dequeue();
            }
            return true;
        }

        /// <summary>
        /// Returns the object at the beginning of the queue without removing it.
        /// </summary>
        /// <returns>The object at the beginning of the queue.</returns>
        /// <remarks>
        /// This method is similar to the Dequeue method, but Peek does not modify the queue. 
        /// A null reference can be added to the Queue as a value. 
        /// To distinguish between a null value and the end of the queue, check the Count property or
        /// catch the InvalidOperationException, which is thrown when the Queue is empty.
        /// </remarks>
        /// <exception cref="InvalidOperationException">The queue is empty.</exception>
        public T Peek()
        {
            lock (_queue.SyncRoot)
            {
                return (T)_queue.Peek();
            }
        }

        /// <summary>
        /// Returns the object at the beginning of the Queue without removing it.
        /// Similar to the Peek method, however this method will not throw exception if
        /// queue is empty, but instead will return false.
        /// </summary>
        /// <param name="value">The object at the beginning of the Queue or null if empty.</param>
        /// <returns>The object at the beginning of the Queue.</returns>
        public bool TryPeek(out T value)
        {
            lock (_queue.SyncRoot)
            {
                if (_queue.Count == 0)
                {
                    value = default(T);
                    return false;
                }
                value = (T)_queue.Peek();
            }
            return true;
        }

        /// <summary>
        /// Removes all objects from the Queue.
        /// </summary>
        /// <remarks>
        /// Count is set to zero. Size does not change.
        /// </remarks>
        public void Clear()
        {
            lock (_queue.SyncRoot)
            {
                _queue.Clear();
            }
        }

        #endregion

        private T dequeue()
        {
            T obj = (T)_queue.Dequeue();
            return obj;
        }

        #region ICollection Members

        /// <summary>
        /// Gets a value indicating whether access to the Queue is synchronized (thread-safe).
        /// </summary>
        public bool IsSynchronized
        {
            get { return true; }
        }

        /// <summary>
        /// Gets the number of elements contained in the Queue.
        /// </summary>
        public int Count
        {
            get
            {
                lock (_queue.SyncRoot)
                {
                    return _queue.Count;
                }
            }
        }

        /// <summary>
        /// Copies the Queue elements to an existing one-dimensional Array,
        /// starting at the specified array index.
        /// </summary>
        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from Queue. The Array must have zero-based indexing.</param>
        /// <param name="index">The zero-based index in array at which copying begins. </param>
        public void CopyTo(Array array, int index)
        {
            lock (_queue.SyncRoot)
            {
                Values.CopyTo(array, index);
            }
        }


        #endregion

        #region IDisposable Members

        /// <summary>
        /// Close down the queue on disposal.
        /// </summary>
        public void Dispose()
        {
            Close();
        }

        #endregion
    }

    /// <summary>
    /// Thrown when the timeout of the blocking queue expires.
    /// </summary>
    public class QueueTimeoutException : Exception
    {
        /// <summary>
        /// Constructs a queue timeout exception
        /// </summary>
        public QueueTimeoutException(string method)
            : base("Queue method timed out on wait: " + method)
        {
        }
    }
}